/*
 * Copyright (c) 2000, 2006, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.management.relation;

import static com.sun.jmx.defaults.JmxProperties.RELATION_LOGGER;
import static com.sun.jmx.mbeanserver.Util.cast;

import com.sun.jmx.mbeanserver.GetPropertyAction;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;

import java.security.AccessController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

/**
 * A RelationTypeSupport object implements the RelationType interface.
 * <P>It represents a relation type, providing role information for each role
 * expected to be supported in every relation of that type.
 *
 * <P>A relation type includes a relation type name and a list of
 * role infos (represented by RoleInfo objects).
 *
 * <P>A relation type has to be declared in the Relation Service:
 * <P>- either using the createRelationType() method, where a RelationTypeSupport
 * object will be created and kept in the Relation Service
 * <P>- either using the addRelationType() method where the user has to create
 * an object implementing the RelationType interface, and this object will be
 * used as representing a relation type in the Relation Service.
 *
 * <p>The <b>serialVersionUID</b> of this class is <code>4611072955724144607L</code>.
 *
 * @since 1.5
 */
@SuppressWarnings("serial")  // serialVersionUID not constant
public class RelationTypeSupport implements RelationType {

  // Serialization compatibility stuff:
  // Two serial forms are supported in this class. The selected form depends
  // on system property "jmx.serial.form":
  //  - "1.0" for JMX 1.0
  //  - any other value for JMX 1.1 and higher
  //
  // Serial version for old serial form
  private static final long oldSerialVersionUID = -8179019472410837190L;
  //
  // Serial version for new serial form
  private static final long newSerialVersionUID = 4611072955724144607L;
  //
  // Serializable fields in old serial form
  private static final ObjectStreamField[] oldSerialPersistentFields =
      {
          new ObjectStreamField("myTypeName", String.class),
          new ObjectStreamField("myRoleName2InfoMap", HashMap.class),
          new ObjectStreamField("myIsInRelServFlg", boolean.class)
      };
  //
  // Serializable fields in new serial form
  private static final ObjectStreamField[] newSerialPersistentFields =
      {
          new ObjectStreamField("typeName", String.class),
          new ObjectStreamField("roleName2InfoMap", Map.class),
          new ObjectStreamField("isInRelationService", boolean.class)
      };
  //
  // Actual serial version and serial form
  private static final long serialVersionUID;
  /**
   * @serialField typeName String Relation type name
   * @serialField roleName2InfoMap Map {@link Map} holding the mapping: &lt;role name ({@link
   * String})&gt; -&gt; &lt;role info ({@link RoleInfo} object)&gt;
   * @serialField isInRelationService boolean Flag specifying whether the relation type has been
   * declared in the Relation Service (so can no longer be updated)
   */
  private static final ObjectStreamField[] serialPersistentFields;
  private static boolean compat = false;

  static {
    try {
      GetPropertyAction act = new GetPropertyAction("jmx.serial.form");
      String form = AccessController.doPrivileged(act);
      compat = (form != null && form.equals("1.0"));
    } catch (Exception e) {
      // OK : Too bad, no compat with 1.0
    }
    if (compat) {
      serialPersistentFields = oldSerialPersistentFields;
      serialVersionUID = oldSerialVersionUID;
    } else {
      serialPersistentFields = newSerialPersistentFields;
      serialVersionUID = newSerialVersionUID;
    }
  }
  //
  // END Serialization compatibility stuff

  //
  // Private members
  //

  /**
   * @serial Relation type name
   */
  private String typeName = null;

  /**
   * @serial {@link Map} holding the mapping: &lt;role name ({@link String})&gt; -&gt; &lt;role info
   * ({@link RoleInfo} object)&gt;
   */
  private Map<String, RoleInfo> roleName2InfoMap =
      new HashMap<String, RoleInfo>();

  /**
   * @serial Flag specifying whether the relation type has been declared in the Relation Service (so
   * can no longer be updated)
   */
  private boolean isInRelationService = false;

  //
  // Constructors
  //

  /**
   * Constructor where all role definitions are dynamically created and
   * passed as parameter.
   *
   * @param relationTypeName Name of relation type
   * @param roleInfoArray List of role definitions (RoleInfo objects)
   * @throws IllegalArgumentException if null parameter
   * @throws InvalidRelationTypeException if: <P>- the same name has been used for two different
   * roles <P>- no role info provided <P>- one null role info provided
   */
  public RelationTypeSupport(String relationTypeName,
      RoleInfo[] roleInfoArray)
      throws IllegalArgumentException,
      InvalidRelationTypeException {

    if (relationTypeName == null || roleInfoArray == null) {
      String excMsg = "Invalid parameter.";
      throw new IllegalArgumentException(excMsg);
    }

    RELATION_LOGGER.entering(RelationTypeSupport.class.getName(),
        "RelationTypeSupport", relationTypeName);

    // Can throw InvalidRelationTypeException, ClassNotFoundException
    // and NotCompliantMBeanException
    initMembers(relationTypeName, roleInfoArray);

    RELATION_LOGGER.exiting(RelationTypeSupport.class.getName(),
        "RelationTypeSupport");
    return;
  }

  /**
   * Constructor to be used for subclasses.
   *
   * @param relationTypeName Name of relation type.
   * @throws IllegalArgumentException if null parameter.
   */
  protected RelationTypeSupport(String relationTypeName) {
    if (relationTypeName == null) {
      String excMsg = "Invalid parameter.";
      throw new IllegalArgumentException(excMsg);
    }

    RELATION_LOGGER.entering(RelationTypeSupport.class.getName(),
        "RelationTypeSupport", relationTypeName);

    typeName = relationTypeName;

    RELATION_LOGGER.exiting(RelationTypeSupport.class.getName(),
        "RelationTypeSupport");
    return;
  }

  //
  // Accessors
  //

  /**
   * Returns the relation type name.
   *
   * @return the relation type name.
   */
  public String getRelationTypeName() {
    return typeName;
  }

  /**
   * Returns the list of role definitions (ArrayList of RoleInfo objects).
   */
  public List<RoleInfo> getRoleInfos() {
    return new ArrayList<RoleInfo>(roleName2InfoMap.values());
  }

  /**
   * Returns the role info (RoleInfo object) for the given role info name
   * (null if not found).
   *
   * @param roleInfoName role info name
   * @return RoleInfo object providing role definition does not exist
   * @throws IllegalArgumentException if null parameter
   * @throws RoleInfoNotFoundException if no role info with that name in relation type.
   */
  public RoleInfo getRoleInfo(String roleInfoName)
      throws IllegalArgumentException,
      RoleInfoNotFoundException {

    if (roleInfoName == null) {
      String excMsg = "Invalid parameter.";
      throw new IllegalArgumentException(excMsg);
    }

    RELATION_LOGGER.entering(RelationTypeSupport.class.getName(),
        "getRoleInfo", roleInfoName);

    // No null RoleInfo allowed, so use get()
    RoleInfo result = roleName2InfoMap.get(roleInfoName);

    if (result == null) {
      StringBuilder excMsgStrB = new StringBuilder();
      String excMsg = "No role info for role ";
      excMsgStrB.append(excMsg);
      excMsgStrB.append(roleInfoName);
      throw new RoleInfoNotFoundException(excMsgStrB.toString());
    }

    RELATION_LOGGER.exiting(RelationTypeSupport.class.getName(),
        "getRoleInfo");
    return result;
  }

  //
  // Misc
  //

  /**
   * Add a role info.
   * This method of course should not be used after the creation of the
   * relation type, because updating it would invalidate that the relations
   * created associated to that type still conform to it.
   * Can throw a RuntimeException if trying to update a relation type
   * declared in the Relation Service.
   *
   * @param roleInfo role info to be added.
   * @throws IllegalArgumentException if null parameter.
   * @throws InvalidRelationTypeException if there is already a role info in current relation type
   * with the same name.
   */
  protected void addRoleInfo(RoleInfo roleInfo)
      throws IllegalArgumentException,
      InvalidRelationTypeException {

    if (roleInfo == null) {
      String excMsg = "Invalid parameter.";
      throw new IllegalArgumentException(excMsg);
    }

    RELATION_LOGGER.entering(RelationTypeSupport.class.getName(),
        "addRoleInfo", roleInfo);

    if (isInRelationService) {
      // Trying to update a declared relation type
      String excMsg = "Relation type cannot be updated as it is declared in the Relation Service.";
      throw new RuntimeException(excMsg);
    }

    String roleName = roleInfo.getName();

    // Checks if the role info has already been described
    if (roleName2InfoMap.containsKey(roleName)) {
      StringBuilder excMsgStrB = new StringBuilder();
      String excMsg = "Two role infos provided for role ";
      excMsgStrB.append(excMsg);
      excMsgStrB.append(roleName);
      throw new InvalidRelationTypeException(excMsgStrB.toString());
    }

    roleName2InfoMap.put(roleName, new RoleInfo(roleInfo));

    RELATION_LOGGER.exiting(RelationTypeSupport.class.getName(),
        "addRoleInfo");
    return;
  }

  // Sets the internal flag to specify that the relation type has been
  // declared in the Relation Service
  void setRelationServiceFlag(boolean flag) {
    isInRelationService = flag;
    return;
  }

  // Initializes the members, i.e. type name and role info list.
  //
  // -param relationTypeName  Name of relation type
  // -param roleInfoArray  List of role definitions (RoleInfo objects)
  //
  // -exception IllegalArgumentException  if null parameter
  // -exception InvalidRelationTypeException  If:
  //  - the same name has been used for two different roles
  //  - no role info provided
  //  - one null role info provided
  private void initMembers(String relationTypeName,
      RoleInfo[] roleInfoArray)
      throws IllegalArgumentException,
      InvalidRelationTypeException {

    if (relationTypeName == null || roleInfoArray == null) {
      String excMsg = "Invalid parameter.";
      throw new IllegalArgumentException(excMsg);
    }

    RELATION_LOGGER.entering(RelationTypeSupport.class.getName(),
        "initMembers", relationTypeName);

    typeName = relationTypeName;

    // Verifies role infos before setting them
    // Can throw InvalidRelationTypeException
    checkRoleInfos(roleInfoArray);

    for (int i = 0; i < roleInfoArray.length; i++) {
      RoleInfo currRoleInfo = roleInfoArray[i];
      roleName2InfoMap.put(currRoleInfo.getName(),
          new RoleInfo(currRoleInfo));
    }

    RELATION_LOGGER.exiting(RelationTypeSupport.class.getName(),
        "initMembers");
    return;
  }

  // Checks the given RoleInfo array to verify that:
  // - the array is not empty
  // - it does not contain a null element
  // - a given role name is used only for one RoleInfo
  //
  // -param roleInfoArray  array to be checked
  //
  // -exception IllegalArgumentException
  // -exception InvalidRelationTypeException  If:
  //  - the same name has been used for two different roles
  //  - no role info provided
  //  - one null role info provided
  static void checkRoleInfos(RoleInfo[] roleInfoArray)
      throws IllegalArgumentException,
      InvalidRelationTypeException {

    if (roleInfoArray == null) {
      String excMsg = "Invalid parameter.";
      throw new IllegalArgumentException(excMsg);
    }

    if (roleInfoArray.length == 0) {
      // No role info provided
      String excMsg = "No role info provided.";
      throw new InvalidRelationTypeException(excMsg);
    }

    Set<String> roleNames = new HashSet<String>();

    for (int i = 0; i < roleInfoArray.length; i++) {
      RoleInfo currRoleInfo = roleInfoArray[i];

      if (currRoleInfo == null) {
        String excMsg = "Null role info provided.";
        throw new InvalidRelationTypeException(excMsg);
      }

      String roleName = currRoleInfo.getName();

      // Checks if the role info has already been described
      if (roleNames.contains(roleName)) {
        StringBuilder excMsgStrB = new StringBuilder();
        String excMsg = "Two role infos provided for role ";
        excMsgStrB.append(excMsg);
        excMsgStrB.append(roleName);
        throw new InvalidRelationTypeException(excMsgStrB.toString());
      }
      roleNames.add(roleName);
    }

    return;
  }


  /**
   * Deserializes a {@link RelationTypeSupport} from an {@link ObjectInputStream}.
   */
  private void readObject(ObjectInputStream in)
      throws IOException, ClassNotFoundException {
    if (compat) {
      // Read an object serialized in the old serial form
      //
      ObjectInputStream.GetField fields = in.readFields();
      typeName = (String) fields.get("myTypeName", null);
      if (fields.defaulted("myTypeName")) {
        throw new NullPointerException("myTypeName");
      }
      roleName2InfoMap = cast(fields.get("myRoleName2InfoMap", null));
      if (fields.defaulted("myRoleName2InfoMap")) {
        throw new NullPointerException("myRoleName2InfoMap");
      }
      isInRelationService = fields.get("myIsInRelServFlg", false);
      if (fields.defaulted("myIsInRelServFlg")) {
        throw new NullPointerException("myIsInRelServFlg");
      }
    } else {
      // Read an object serialized in the new serial form
      //
      in.defaultReadObject();
    }
  }


  /**
   * Serializes a {@link RelationTypeSupport} to an {@link ObjectOutputStream}.
   */
  private void writeObject(ObjectOutputStream out)
      throws IOException {
    if (compat) {
      // Serializes this instance in the old serial form
      //
      ObjectOutputStream.PutField fields = out.putFields();
      fields.put("myTypeName", typeName);
      fields.put("myRoleName2InfoMap", roleName2InfoMap);
      fields.put("myIsInRelServFlg", isInRelationService);
      out.writeFields();
    } else {
      // Serializes this instance in the new serial form
      //
      out.defaultWriteObject();
    }
  }
}
